package lumis.portal.audit;

import lumis.portal.AccessDeniedException;
import lumis.portal.PortalException;
import lumis.portal.acl.PortalPermissions;
import lumis.portal.authentication.SessionConfig;
import lumis.portal.manager.ManagerFactory;
import lumis.portal.transaction.AbstractTransactionObserver;
import lumis.portal.transaction.PortalTransactionFactory;
import lumis.portal.user.UserConfig;
import lumis.util.ITransaction;
import lumis.util.log.ILogger;
import lumis.util.log.LoggerFactory;

import java.util.Calendar;

/**
 * Manager for Auditing.
 * 
 * @version $Revision: 13093 $ $Date: 2011-05-28 18:40:18 -0300 (Sat, 28 May 2011) $
 * @since 6.0.0
 */
public class AuditManager implements IAuditManager
{
	// ************************************************************************
	// PUBLIC
	// ************************************************************************

	private static final ILogger logger = LoggerFactory.getLogger(AuditManager.class);

	/**
	 * Enum of audit types used by audit actions taken.
	 * 
	 * @version $Revision: 13093 $ $Date: 2011-05-28 18:40:18 -0300 (Sat, 28 May 2011) $
	 * @since 6.0.0
	 */
	public static enum AuditType
	{
		DISABLE_AUDIT_ENTRY_TYPE("lumis.portal.audit.AuditEntryType#disable", "Disable Audit Entry Type"),
		ENABLE_AUDIT_ENTRY_TYPE("lumis.portal.audit.AuditEntryType#enable", "Enable Audit Entry Type");
		private final String key, title;

		private AuditType(String key, String title)
		{
			this.key = key;
			this.title = title;
		}

		/**
		 * Returns the key.
		 * 
		 * @return the key
		 * @since 6.0.0
		 */
		public String getKey()
		{
			return key;
		}

		/**
		 * Returns the title.
		 * 
		 * @return the title
		 * @since 6.0.0
		 */
		public String getTitle()
		{
			return title;
		}
	}
	
	/**
	 * Register an audit entry.
	 * @param title				the operation title. If the title is bigger
	 * 							than the allowed size, the string is truncated. 
	 * @param description		the operation description.
	 * @param typeKey			the AuditEntryType key.
	 * @throws PortalException
	 * @since 6.0.0
	 */
	public void audit(String title, String description, String typeKey)
						throws PortalException 
	{		
		ITransaction transaction = PortalTransactionFactory.createTransaction();
		try
		{
			transaction.begin();
						
			AuditEntryType type = findEntryTypeByKey(typeKey);
			
			UserConfig userConfig = ManagerFactory.getUserManager().get(SessionConfig
																			.getCurrentSessionConfig(), 
																		SessionConfig
																			.getCurrentSessionConfig().getUserId(),
																		transaction);
			
			
			AuditEntry auditEntry = initEntry(userConfig.getId(),
												userConfig.getLogin(),
												title,
												description,
												type,
												false);
			
			logEntry(auditEntry);
			
			registerEntry(auditEntry, AuditEntryState.COMPLETED);

			transaction.commit();
		}
		finally
		{
			transaction.dispose();
		}	
	}
	
	/**
	 * Starts an operation for audit and add register an observer to the
	 * transaction in order to end the operation.
	 * The entry is registered in database as initialized.
	 * @param title				the operation title.
	 * @param description		the operation description.
	 * @param categoryKey		the AuditCategory key.
	 * @param typeKey			the AuditEntryType key.
	 * @param registerChilds	true if the child entries must be saved in
	 * 							database, false otherwise.
	 * @return					an instance of AuditEntry representing the
	 * 							operation.
	 * @since 6.0.0
	 */
	public AuditEntry audit(String title,
									String description,
									String typeKey,
									boolean registerChilds,
									ITransaction t)
							throws PortalException
	{
		ITransaction transaction = PortalTransactionFactory.createTransaction();
		try
		{
			transaction.begin();
						
			AuditEntryType type = findEntryTypeByKey(typeKey);
			
			UserConfig userConfig = ManagerFactory.getUserManager().get(SessionConfig
																			.getCurrentSessionConfig(), 
																		SessionConfig
																			.getCurrentSessionConfig().getUserId(),
																		transaction);

			
			final AuditEntry auditEntry = initEntry(SessionConfig
													.getCurrentSessionConfig()
													.getUserId(),
												userConfig.getLogin(),
												title,
												description,
												type,
												registerChilds);
			
			initContext(auditEntry, registerChilds);
			
			logEntry(auditEntry);
			
			registerEntry(auditEntry, AuditEntryState.STARTED);
			
			// add the observer
			t.addObserver(new AbstractTransactionObserver()
			{
				@Override
				public void afterCommit() throws PortalException
				{
					endAudit(auditEntry);
				}
			});
			
			transaction.commit();
			
			return auditEntry;
		}
		finally
		{
			transaction.dispose();
		}
	}
	
	/**
	 * Starts an operation for audit.
	 * The entry is registered in database as initialized.
	 * @param title				the operation title.
	 * @param description		the operation description.
	 * @param categoryKey		the AuditCategory key.
	 * @param typeKey			the AuditEntryType key.
	 * @param registerChilds	true if the child entries must be saved in
	 * 							database, false otherwise.
	 * @return					an instance of AuditEntry representing the
	 * 							operation.
	 * @since 6.0.0
	 */
	public AuditEntry beginAudit(String title,
									String description,
									String typeKey,
									boolean registerChilds)
							throws PortalException
	{
		ITransaction transaction = PortalTransactionFactory.createTransaction();
		try
		{
			transaction.begin();
						
			AuditEntryType type = findEntryTypeByKey(typeKey);
			
			UserConfig userConfig = ManagerFactory.getUserManager().get(SessionConfig
													.getCurrentSessionConfig(), 
													SessionConfig
													.getCurrentSessionConfig().getUserId(),
													transaction);
			
			AuditEntry auditEntry = initEntry(SessionConfig
													.getCurrentSessionConfig()
													.getUserId(),
												userConfig.getLogin(),
												title,
												description,
												type,
												registerChilds);
			
			initContext(auditEntry, registerChilds);
			
			logEntry(auditEntry);
			
			registerEntry(auditEntry, AuditEntryState.STARTED);
			
			transaction.commit();
			
			return auditEntry;
		}
		catch (PortalException e)
		{
			// TODO 
			e.printStackTrace();
			throw e;
		}
		finally
		{
			transaction.dispose();
		}
	}
	
	/**
	 * Ends the AuditEntry operation.
	 * @param auditEntry	the AuditEntry instance.
	 * @since 6.0.0
	 */
	public void endAudit(AuditEntry auditEntry) throws PortalException
	{
		ITransaction transaction = PortalTransactionFactory.createTransaction();
		try
		{
			transaction.begin();

			registerEntry(auditEntry, AuditEntryState.COMPLETED);
			
			endContext(auditEntry);
			
			transaction.commit();
		}
		catch (PortalException e)
		{
			// TODO 
			e.printStackTrace();
			throw e;
		}
		finally
		{
			transaction.dispose();
		}
	}
	
	// ************************************************************************
	// AUDIT CONFIGURATION
	// ************************************************************************

	/**
	 * Enable the {@link AuditEntryType} by it's id.
	 * @param auditEntryTypeId	the type id.
	 * @throws AccessDeniedException 	if the user has not permission to use
	 * 									management functions.
	 * @since 6.0.0
	 */
	public void enableType(String auditEntryTypeId) throws PortalException
	{
		checkManagePermission();
		
		AuditEntryType type = ManagerFactory.getEntityManager().find(AuditEntryType.class, auditEntryTypeId);
		if (type.isEnabled())
			return;

		// audit change
		auditAuditEntryTypeAction(AuditType.ENABLE_AUDIT_ENTRY_TYPE, auditEntryTypeId);
		type.setEnabled(true);
	}
	
	/**
	 * Disable the {@link AuditEntryType} by it's id.
	 * @param auditEntryTypeId	the type id.
	 * @throws AccessDeniedException 	if the user has not permission to use
	 * 									management functions.
	 * @since 6.0.0
	 */
	public void disableType(String auditEntryTypeId) throws PortalException
	{
		checkManagePermission();
		
		AuditEntryType type = ManagerFactory.getEntityManager().find(AuditEntryType.class, auditEntryTypeId);
		if (!type.isEnabled())
			return;

		// audit change
		auditAuditEntryTypeAction(AuditType.DISABLE_AUDIT_ENTRY_TYPE, auditEntryTypeId);
		type.setEnabled(false);	
	}
	
	/**
	 * Remove audit entries older than the number of days specified by the
	 * cleanInterval field in configuration.
	 * @since 6.0.0
	 */
	public void removeOldEntries() throws PortalException
	{
		AuditConfig auditConfig = obtainConfiguration();
		
		Calendar date = Calendar.getInstance();
		date.add(Calendar.DATE, -auditConfig.getCleanInterval());
		
		AuditEntryDao auditEntryDao = new AuditEntryDao();
		auditEntryDao.deleteOldEntries(date.getTime());
	}
	
	/**
	 * Updates the time interval to clean audit entries.
	 * @param cleanInterval				the new value for time interval in
	 * 									days.
	 * @throws AccessDeniedException 	if the user has not permission to use
	 * 									management functions.
	 * @throws PortalException			if an error occur updating the time
	 * 									interval.
	 * @since 6.0.0
	 */
	public void updateCleanInterval(int cleanInterval) throws PortalException
	{
		checkManagePermission();
		
		AuditConfigDao auditConfigDao = new AuditConfigDao();
		AuditConfig auditConfig = auditConfigDao.findConfiguration();
		
		auditConfig.setCleanInterval(cleanInterval);
	}
	
	/**
	 * Obtain the audit configuration.
	 * @return	an instance of {@link AuditConfig}.
	 * @throws PortalException	if an error occur retrieving the configuration.
	 * @since 6.0.0
	 */
	public AuditConfig obtainConfiguration() throws PortalException
	{
		AuditConfigDao auditConfigDao = new AuditConfigDao();
		return auditConfigDao.findConfiguration();
	}

	/**
	 * Clears the {@link AuditContext} {@link Thread thread} data.
	 * 
	 * @since 6.0.0
	 */
	public void clearContextThreadData()
	{
		AuditContext.clearThreadData();
	}

	// ************************************************************************
	// PROTECTED
	// ************************************************************************
	
	/**
	 * Creates an instance of AuditEntry.
	 * @param user				user.
	 * @param title				a title for the operation being audited.
	 * 							If the title is bigger than the allowed, a
	 * 							substring is created.
	 * @param description		a description for the operation.
	 * @param type				the AuditEntryType instance.
	 * @param registerChilds	true if the child entries must be saved in
	 * 							database, false otherwise.
	 * @return					an instance of AuditEntry.
	 * @since 6.0.0
	 */
	protected AuditEntry initEntry(String userId,
									String userLogin,
									String title,
									String description,
									AuditEntryType type,
									boolean registerChilds)
	{
		boolean saveEntry = true;		
		if (AuditContext.getCurrentContext() != null)
		{
			// get the registerChilds of the parent context
			saveEntry = AuditContext.getCurrentContext().isRegisterChilds();
		}
		if (saveEntry)
		{
			saveEntry = type.getCategory().isEnabled() && type.isEnabled();
		}
		
		if ((title != null) && (title.length() > 255))
		{
			title = title.substring(0, 255);
		}
		
		AuditEntry auditEntry = new AuditEntry(userId,
												userLogin,
												title, 
												description,
												type,
												saveEntry);
		return auditEntry;
	}
	
	/**
	 * Find the AuditCategory by it's key.
	 * @param categoryKey	the audit category key.
	 * @return				the instance of AuditCategory.
	 * @throws PortalException	if an error occur in audit category dao.
	 * @since 6.0.0
	 */
	protected AuditCategory findCategoryByKey(String categoryKey) throws PortalException
	{
		AuditCategoryDao categoryDao = new AuditCategoryDao();
		return categoryDao.findByKey(categoryKey);
	}

	/**
	 * Find the category by it's id.
	 * @param categoryId		the category id.
	 * @return					the AuditCategory instance.
	 * @throws PortalException	
	 * @since 6.0.0
	 */
	protected AuditCategory findCategoryById(String categoryId) throws PortalException
	{
		
		AuditCategory auditCategory = ManagerFactory.getEntityManager().find(AuditCategory.class, categoryId);
		return auditCategory;
	}

	/**
	 * Find the AuditEntryType by it's key.
	 * @param entryTypeKey		the audit entry type key.
	 * @return					the instance of AuditEntryType.
	 * @throws PortalException	if an error occur in audit entry type dao.
	 * @since 6.0.0
	 */
	protected AuditEntryType findEntryTypeByKey(String entryTypeKey) throws PortalException
	{
		AuditEntryTypeDao entryTypeDao = new AuditEntryTypeDao();
		return entryTypeDao.findByKey(entryTypeKey);
	}

	/**
	 * Initializes the audit context.
	 * @param auditEntry		the AuditEntry instance.
	 * @param registerChilds	true if the child entries must be saved in
	 * 							database, false otherwise.
	 * @since 6.0.0
	 */
	protected void initContext(AuditEntry auditEntry, boolean registerChilds)
	{
		// creates the new context
		AuditContext auditContext = new AuditContext();
		auditContext.setEntry(auditEntry);
		
		if (AuditContext.getCurrentContext() != null)
		{
			// verifies the registerChilds value from the parent context
			registerChilds = registerChilds && AuditContext.getCurrentContext().isRegisterChilds();
			
			// sets the parent context
			if (AuditContext.getCurrentContext().isDifferentThread())
			{
				auditContext.setContextParentThread(AuditContext.getCurrentContext());
			}
			else
			{
				// add the entry as a child of the current
				AuditContext.getCurrentContext().addChild(auditContext);
			}
			auditEntry.setParentId(AuditContext.getCurrentContext().getEntry().getId());			
		}
		auditContext.setRegisterChilds(registerChilds);
		
		AuditContext.setCurrentContext(auditContext);
	}

	/**
	 * End the audit context.
	 * @param auditEntry	the AuditEntry instance.
	 * @since 6.0.0
	 */
	protected void endContext(AuditEntry auditEntry)
	{
		if (AuditContext.getCurrentContext().getParent() != null)
		{
			AuditContext.setCurrentContext(AuditContext.getCurrentContext().getParent());
		}
		else
		{
			AuditContext.setCurrentContext(null);
		}
	}

	/**
	 * Log the audit entry.
	 * @param auditEntry	the AuditEntry instance.
	 * @since 6.0.0
	 */
	protected void logEntry(AuditEntry auditEntry)
	{
		logger.debug("Category: " + auditEntry.getType().getCategory().getName() 
					+ " - Type: " + auditEntry.getType().getName() 
					+ " - User: " + auditEntry.getUserId() + " " + auditEntry.getUserLogin() 
					+ " - Title: '" + auditEntry.getTitle() + "'"
					+ " - Description: '" + auditEntry.getDescription() + "'");
	}
	
	/**
	 * Register the audit entry in database.
	 * @param auditEntry		the AuditEntry instance.
	 * @param state				the state of audit entry.
	 * @since 6.0.0
	 */
	protected void registerEntry(AuditEntry auditEntry, AuditEntryState state)
						throws PortalException
	{	
		if (auditEntry.isRegister())
		{
			ITransaction transaction = PortalTransactionFactory.createTransaction();
			try
			{
				transaction.begin();
				
				auditEntry.setState(state);
				if (auditEntry.getId() != null)
				{
					auditEntry = ManagerFactory.getEntityManager().merge(auditEntry);
				}
				else
				{
					ManagerFactory.getEntityManager().persist(auditEntry);
				}
				transaction.commit();
			}
			finally
			{
				transaction.dispose();
			}
		}
		else
		{
			// if the entry will not be saved, updates the state
			auditEntry.setState(state);
		}
	}
	// ************************************************************************
	// PRIVATE
	// ************************************************************************
	
	/**
	 * Check if user has permission to use management functions.
	 * @throws AccessDeniedException if the user has not permission to use management functions.
	 * @since 6.0.0
	 */
	private void checkManagePermission() throws PortalException
	{	 
		if (!ManagerFactory.getPortalAclManager()
								.checkPermission(
										SessionConfig.getCurrentSessionConfig(),
										PortalPermissions.MANAGE_PORTAL,
										PortalTransactionFactory.getCurrentTransaction())) {
			throw new AccessDeniedException();
		}
	}

	/**
	 * Audit changes in {@link AuditEntryType}.
	 * 
	 * @param auditType
	 *            the {@link AuditType}.
	 * @param auditEntryTypeId
	 *            the {@link AuditEntryType} id.
	 * @since 6.0.0
	 */
	private void auditAuditEntryTypeAction(AuditType auditType, String auditEntryTypeId) throws PortalException
	{
		AuditEntryType auditEntryType = ManagerFactory.getEntityManager().find(AuditEntryType.class, auditEntryTypeId);
		if (auditEntryType != null)
		{
			String title = auditType.title + ": " + auditEntryType.getName();
			String description = title + "\r\n  id: " + auditEntryType.getId() + "\r\n  key: " + auditEntryType.getKey() + "\r\n  description: " + auditEntryType.getDescription() + "\r\n  category: "
					+ auditEntryType.getCategory().getName();
			audit(title, description, auditType.key);
		}
		else
		{
			throw new IllegalArgumentException("AuditEntryType not found. Id: " + auditEntryTypeId);
		}
	}
}